package com.naiterui.ehp.bs.esign.api.scca.service.impl;

import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import com.google.common.collect.Maps;
import com.naiterui.common.redis.RedisUtil;
import com.naiterui.ehp.bp.constants.CommonConstant;
import com.naiterui.ehp.bp.constants.EsignServiceProviderEnum;
import com.naiterui.ehp.bp.support.exception.BusinessException;
import com.naiterui.ehp.bp.utils.lang.ConfigUtil;
import com.naiterui.ehp.bp.utils.sms.SMSUtil;
import com.naiterui.ehp.bp.utils.thread.ThreadPoolUtil;
import com.naiterui.ehp.bs.esign.api.ISignService;
import com.naiterui.ehp.bs.esign.api.netrain.utils.NetrainPdfSignUtil;
import com.naiterui.ehp.bs.esign.api.netrain.utils.SealImageUtil;
import com.naiterui.ehp.bs.esign.api.scca.entity.EsignSccaCert;
import com.naiterui.ehp.bs.esign.api.scca.entity.EsignSccaSignInfo;
import com.naiterui.ehp.bs.esign.api.scca.repository.IEsignSccaCertRepository;
import com.naiterui.ehp.bs.esign.api.scca.repository.IEsignSccaSignInfoRepository;
import com.naiterui.ehp.bs.esign.api.scca.util.BcCertUtil;
import com.naiterui.ehp.bs.esign.api.scca.util.SignSccaUtil;
import com.naiterui.ehp.bs.esign.api.scca.vo.SccaCertInfoVO;
import com.naiterui.ehp.bs.esign.api.scca.vo.SccaCertSignVO;
import com.naiterui.ehp.bs.esign.api.scca.vo.SccaCertVO;
import com.naiterui.ehp.bs.esign.api.vo.SignAccountVO;
import com.naiterui.ehp.bs.esign.api.vo.SignCallbackVO;
import com.naiterui.ehp.bs.esign.api.vo.SignFileParamVO;
import com.naiterui.ehp.bs.esign.api.vo.SignUserInfoVO;
import com.naiterui.ehp.bs.esign.api.vo.UserInfoParamVO;
import com.naiterui.ehp.bs.esign.exception.ExceptionCodes;
import com.naiterui.ehp.bs.esign.service.IEsignService;
import com.naiterui.ehp.bs.esign.vo.esign.PreSignResultVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

/**
 * 四川CA电子签名实现
 *
 * @author guoyongxiang
 * @version 1.0.0
 * @date 2021/2/23 17:15
 **/
@Slf4j
public class SignServiceImpl implements ISignService {

    public static final String PROVINCE = "四川";
    public static final String LOCALITY = "成都";

    /**
     * 三要素实名认证流程id
     */
    private final static String REALNAME_CODE_FLOWID = CommonConstant.REDIS_PRE_ESIGN + "phone_";
    /**
     * 实名认证短信验证码发送缓存
     */
    public static final String REALNAME_MESSAGE_CODE_PHONE =
        CommonConstant.REDIS_PRE_ESIGN + "realname_message_code_phone_";

    /**
     * 实名认证短信验证码可重复发送间隔
     */
    public static final int REALNAME_MESSAGE_CODE_EXIPRED_SECOND = 60;

    public static final String CODE_CACHE = CommonConstant.REDIS_PRE_ESIGN + "realname_code_flowId_";

    /**
     * 证书自动续期时间(证书失效前{@value}天)
     */
    private static final int CERT_AUTO_RENEW_BEFORE_DAY = 5;

    @Value("${file.base.prefix}")
    private String basePath;

    @Autowired
    private IEsignSccaSignInfoRepository esignSccaSignInfoRepository;
    @Autowired
    private IEsignSccaCertRepository esignSccaCertRepository;

    @Override
    public EsignServiceProviderEnum getCurrentServiceProvider() {
        return EsignServiceProviderEnum.SCCA;
    }

    @Override
    public SealPosConfig getSealPosConfig() {
        return new SealPosConfig() {

            @Override
            public SealPosVO getPrPosDr() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(135f).y(140f).build();
            }

            @Override
            public SealPosVO getPrPosPh() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(500f).y(140f).build();
            }

            @Override
            public SealPosVO getPrPosAlPh() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(150f).y(105f).build();
            }

            @Override
            public SealPosVO getEmrPos() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(460f).y(110f).build();
            }

            @Override
            public SealPosVO getNsPosDr() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(135f).y(140f).build();
            }

            @Override
            public SealPosVO getNsPosPh() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(500f).y(140f).build();
            }

            @Override
            public SealPosVO getGcPosSource() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(480f).y(600f).build();
            }

            @Override
            public SealPosVO getGcPosTarget() {
                return SealPosVO.builder().type(SealPosVO.TYPE_GRID).x(480f).y(400f).build();
            }
        };
    }

    @Override
    public SignUserInfoVO registAccount(UserInfoParamVO userInfoParamVO) throws BusinessException {
        log.info("四川CA同步用户信息：userinfoParamVO={}", userInfoParamVO.toString());

        String openId = this.getOpenId(userInfoParamVO.getIdNo());
        // 创建签名证书
        SccaCertVO sccaCertVO = SignSccaUtil
            .cloudCertEnroll(SccaCertInfoVO.builder().entityId(openId).entityType(SignSccaUtil.CERT_TYPE_PERSONAL)
                .personalName(userInfoParamVO.getName()).personalPhone(userInfoParamVO.getPhone())
                .personalIdNumber(userInfoParamVO.getIdNo()).personalEmail(userInfoParamVO.getName())
                .orgName(userInfoParamVO.getOrgan()).province(PROVINCE).locality(LOCALITY)
                // .pin(SignSccaUtil.DEF_PASSWORD)
                .pin(userInfoParamVO.getPin()).build());

        // 读取证书信息
        X509Certificate x509Certificate = this.getCertInfoByBase64(sccaCertVO.getCertBase64());

        // 保存证书信息
        esignSccaCertRepository.save(EsignSccaCert.builder().openId(openId).name(userInfoParamVO.getName())
            .cardType(userInfoParamVO.getCerType()).cardNo(userInfoParamVO.getIdNo())
            .certSerialnumber(sccaCertVO.getCertSerialnumber()).certBase64(sccaCertVO.getCertBase64())
            .certP7(sccaCertVO.getCertP7()).notBefore(x509Certificate.getNotBefore())
            .notAfter(x509Certificate.getNotAfter()).createdAt(new Date()).changedAt(new Date()).build());
        return SignUserInfoVO.builder().openId(openId).callbackType(SignUserInfoVO.TYPE_CALLBACK_INTERFACE).build();
    }

    @Override
    public SignUserInfoVO updateAccount(UserInfoParamVO userInfoParamVO) throws BusinessException {
        log.info("四川CA更新用户信息：userinfoParamVO={}", userInfoParamVO.toString());

        String openId = this.getOpenId(userInfoParamVO.getIdNo());
        SccaCertVO sccaCertVO = SignSccaUtil
            .cloudCertChange(SccaCertInfoVO.builder().entityId(openId).entityType(SignSccaUtil.CERT_TYPE_PERSONAL)
                .personalName(userInfoParamVO.getName()).personalPhone(userInfoParamVO.getPhone())
                .personalIdNumber(userInfoParamVO.getIdNo()).personalEmail(userInfoParamVO.getName())
                .orgName(userInfoParamVO.getOrgan()).province(PROVINCE).locality(LOCALITY)
                // TODO 获取上次密码
                .pin(userInfoParamVO.getPin()).build());

        // 读取证书信息
        X509Certificate x509Certificate = this.getCertInfoByBase64(sccaCertVO.getCertBase64());

        // 保存证书信息
        EsignSccaCert sccaCert = esignSccaCertRepository.findByOpenId(openId);
        sccaCert.setOpenId(openId);
        sccaCert.setName(userInfoParamVO.getName());
        sccaCert.setCardType(userInfoParamVO.getCerType());
        sccaCert.setCardNo(userInfoParamVO.getIdNo());
        sccaCert.setCertSerialnumber(sccaCertVO.getCertSerialnumber());
        sccaCert.setCertBase64(sccaCertVO.getCertBase64());
        sccaCert.setCertP7(sccaCertVO.getCertP7());
        sccaCert.setNotBefore(x509Certificate.getNotBefore());
        sccaCert.setNotAfter(x509Certificate.getNotAfter());
        sccaCert.setChangedAt(new Date());
        esignSccaCertRepository.update(sccaCert);

        return SignUserInfoVO.builder().openId(openId).callbackType(SignUserInfoVO.TYPE_CALLBACK_INTERFACE).build();
    }

    @Override
    public void deleteAccount(String accountId) throws BusinessException {
        log.info("四川CA删除用户信息：accountId={}", accountId);

        SignSccaUtil.cloudCertRevoke(accountId, SignSccaUtil.DEF_PASSWORD);
    }

    @Override
    public String getRealnameCode(String name, String phone, String idCard) throws BusinessException {
        // 发送并记录验证码
        // 校验是否是1分钟内重复提交实名申请
        if (RedisUtil.keyOps().existsKey(REALNAME_MESSAGE_CODE_PHONE + phone)) {
            throw new BusinessException(ExceptionCodes.ESIGN_REALNAME_MESSAGE_CODE_REPEAT);
        }
        // 业务流程id
        String flowId = this.sendSmsCode(phone);

        RedisUtil.valueOps().set(REALNAME_CODE_FLOWID + phone, flowId);
        RedisUtil.keyOps().expire(REALNAME_MESSAGE_CODE_PHONE + phone, REALNAME_MESSAGE_CODE_EXIPRED_SECOND);
        return flowId;
    }

    @Override
    public boolean checkRealnameCode(String phone, String code) throws BusinessException {
        String flowId = RedisUtil.valueOps().getString(REALNAME_CODE_FLOWID + phone);
        if (flowId == null) {
            throw new BusinessException(ExceptionCodes.ESIGN_REALNAME_AUTH_FAIL);
        }

        return this.verifySmsCode(flowId, code);
    }

    @Override
    public String createSeal(String accountId, String name, String typeface) throws BusinessException {
        log.warn("四川CA创建签章，生成机打签章：accountId={}", accountId);
        String path = "/tmp/" + UUID.randomUUID() + ".png";
        try {
            boolean result = SealImageUtil.nameSealGeneration(name, path, Color.BLACK, typeface);
            if (result) {
                byte[] b = Files.readAllBytes(Paths.get(path));
                return Base64.getEncoder().encodeToString(b);
            } else {
                return null;
            }
        } catch (Exception e) {
            log.error("生成电子签名签章图异常 accountId:{}, name:{}", accountId, name, e);
            throw new BusinessException(ExceptionCodes.FAILED, "生成姓名签章图片异常");
        }
    }

    @Override
    public PreSignResultVO preSign(SignFileParamVO signParamVO, SignAccountVO signAccountVO) throws BusinessException {
        PreSignResultVO resultVO = new PreSignResultVO();
        // 预签名唯一id
        String uniqueId = UUID.randomUUID().toString();
        resultVO.setUniqueId(uniqueId);
        return resultVO;
    }

    @Override
    public void sign(SignFileParamVO signParamVO, SignAccountVO signAccountVO, IEsignService callback)
        throws BusinessException {

        EsignSccaCert sccaCert = esignSccaCertRepository.findByOpenId(signAccountVO.getOpenId());
        if (sccaCert != null) {
            // 证书有效期检测(同步,场景：已经失效的证书在签名前触发重新续期)
            this.checkCertExpireAndUpdate(signAccountVO.getOpenId(), sccaCert.getCertBase64(), signAccountVO.getPin(),
                CERT_AUTO_RENEW_BEFORE_DAY);
        }

        File file = new File(signParamVO.getPdfPath());
        if (!file.exists()) {
            log.warn("四川CA签名-待签源文件不存在 path:{}, urId:{}", signParamVO.getPdfPath(), signParamVO.getUrId());
            throw new BusinessException(ExceptionCodes.ESIGN_PRESCRIPTION_FILE_NOT_FOUND);
        }

        // 合成签名图PDF
        String signedPdfBase64 = NetrainPdfSignUtil.pdfSign(signAccountVO.getUserType(),
            basePath + signAccountVO.getXingkaiSealImage(), signParamVO.getPdfPath(), signParamVO.getOutputPath(),
            basePath + signAccountVO.getSealImage(), (int)(float)signParamVO.getX(), (int)(float)signParamVO.getY());

        // 获取文件摘要
        String sha1Hex;
        try {
            sha1Hex = DigestUtils.sha1Hex(new FileInputStream(file));
        } catch (IOException e) {
            log.error("四川CA签名-文件获取sha1Hex异常 path:{}, urId:{}", signParamVO.getPdfPath(), signParamVO.getUrId(), e);
            throw new BusinessException(ExceptionCodes.ESIGN_SIGN_FIALED, "待签名文件获取摘要信息失败");
        }
        log.info("四川CA签名-获取待签文件摘要 path:{}, urId:{}, sha1Hex:{}, openId:{}", signParamVO.getPdfPath(),
            signParamVO.getUrId(), sha1Hex, signAccountVO.getOpenId());

        // 摘要签
        SccaCertSignVO certSignVO =
            SignSccaUtil.cloudCertSign(signAccountVO.getOpenId(), signAccountVO.getPin(), sha1Hex);

        // 保存签名信息
        esignSccaSignInfoRepository.save(EsignSccaSignInfo.builder().signId(signParamVO.getUrId())
            .openId(signAccountVO.getOpenId()).sha1Hash(sha1Hex).signMethod(certSignVO.getSignMethod())
            .signP7(certSignVO.getSignP7()).certSerialnumber(certSignVO.getCertSerialnumber())
            .signBare(certSignVO.getSignBare()).certBase64(certSignVO.getCertBase64()).createdAt(new Date()).build());

        // 签名回调
        callback.signResult(SignCallbackVO.builder().openId(signAccountVO.getOpenId()).urId(signParamVO.getUrId())
            .signedData(certSignVO.getSignP7()).signedPdfBase64(signedPdfBase64).status(SignCallbackVO.STATUS_SIGNED)
            .uniqueId(signParamVO.getUnionId()).signTime(new Date()).sealImage(signAccountVO.getSealImage()).build());

        // 检查签名证书有效期（异步，场景：即将失效的证书在签名前触发重新续期）
        this.checkCertExpireAndUpdateAsync(signAccountVO.getOpenId(), certSignVO.getCertBase64(),
            signAccountVO.getPin(), CERT_AUTO_RENEW_BEFORE_DAY);
    }

    @Override
    public void resetSignPwd(String openId, String newPassword) throws BusinessException {
        log.info("四川CA设置签名证书PIN");
        SignSccaUtil.cloudCertPinReset(openId, newPassword);
    }

    /**
     * 发送验证码并缓存
     *
     * @param phone
     * @return
     */
    private String sendSmsCode(String phone) {
        String code = RandomStringUtils.random(4, false, true);
        SMSUtil.sendSMS(phone, SMSUtil.Template.PWD, code);
        String flowId = UUID.randomUUID().toString();
        RedisUtil.valueOps().setString(CODE_CACHE + flowId, code, REALNAME_MESSAGE_CODE_EXIPRED_SECOND);
        return flowId;
    }

    private boolean verifySmsCode(String flowId, String code) {
        String rdsCode = RedisUtil.valueOps().getString(CODE_CACHE + flowId);
        return code.equals(rdsCode);
    }

    private X509Certificate getCertInfoByBase64(String certBase64) throws BusinessException {
        // 读取证书信息
        X509Certificate x509Certificate;
        try {
            x509Certificate = BcCertUtil.getCertInfoByBase64(certBase64);
        } catch (CertificateException e) {
            log.error("四川CA同步用户信息，证书文件解码异常", e);
            throw new BusinessException(ExceptionCodes.ESIGN_USER_SYNC_FAILED, "电子签名证书文件解码异常", e);
        } catch (NoSuchProviderException e) {
            log.error("四川CA同步用户信息，证书文件解码异常，不支持的服务方", e);
            throw new BusinessException(ExceptionCodes.ESIGN_USER_SYNC_FAILED, "电子签名证书文件解码异常，不支持的服务方");
        }
        return x509Certificate;
    }

    private String getOpenId(String idCard) {
        String pre = ConfigUtil.getString("esign.scca.userIdPre");
        return pre + idCard;
    }

    private String getIdCardByOpenId(String openId) {
        String pre = ConfigUtil.getString("esign.scca.userIdPre");
        return openId.replaceAll(pre, "");
    }

    /**
     * 异步检查证书有效期并续期
     *
     * @param openId
     * @param certBase64
     * @param pin
     */
    private void checkCertExpireAndUpdateAsync(String openId, String certBase64, String pin, Integer beforeDays) {
        ThreadPoolUtil.execute(() -> {
            try {
                this.checkCertExpireAndUpdate(openId, certBase64, pin, beforeDays);
            } catch (BusinessException e) {
            }
        });
    }

    /**
     * 检查证书有效期并续期
     *
     * @param openId
     * @param certBase64
     * @param pin
     */
    private void checkCertExpireAndUpdate(String openId, String certBase64, String pin, Integer beforeDays)
        throws BusinessException {
        X509Certificate x509Certificate = null;
        EsignSccaCert sccaCert = esignSccaCertRepository.findByOpenId(openId);
        if (sccaCert == null || sccaCert.getNotAfter() == null) {
            try {
                x509Certificate = this.getCertInfoByBase64(certBase64);
            } catch (BusinessException e) {
                log.warn("证书有效期校验-证书信息解析失败 openId:{}, certBase64:{}", openId, certBase64);
                throw e;
            }
        }

        if (sccaCert == null) {
            log.warn("证书有效期校验-数据异常，签名证书记录缺失，自动补全账户数据 openId:{}", openId);
            Map<String, String> subjectDNMap = this.getCertSubjectDNMap(x509Certificate.getSubjectDN().getName());

            sccaCert = EsignSccaCert.builder().openId(openId).name(subjectDNMap.get("CN"))
                .cardNo(this.getIdCardByOpenId(openId)).cardType(UserInfoParamVO.CER_TYPE_IDCARD)
                .notBefore(x509Certificate.getNotBefore()).notAfter(x509Certificate.getNotAfter())
                .certSerialnumber(x509Certificate.getSerialNumber().toString(16).toUpperCase()).certP7("")
                .certBase64(certBase64).createdAt(new Date()).changedAt(new Date()).build();
            this.esignSccaCertRepository.save(sccaCert);
        }

        if (sccaCert.getNotAfter() == null) {
            log.warn("证书有效期校验-失效时间缺失，数据自动补全更新 openId:{}, expireAt:{}", openId, x509Certificate.getNotAfter());
            sccaCert.setNotBefore(x509Certificate.getNotBefore());
            sccaCert.setNotAfter(x509Certificate.getNotAfter());
            sccaCert.setChangedAt(new Date());
            this.esignSccaCertRepository.update(sccaCert);
            return;
        }
        // 证书即将过期，自动续期证书并保存新的信息
        this.certRenewal(openId, pin, sccaCert.getNotAfter(), beforeDays);
    }

    private void certRenewal(String openId, String pin, Date expireAt, Integer beforeDays) throws BusinessException {
        long betweenDay = DateUtil.between(new Date(), expireAt, DateUnit.DAY, false);
        log.info("证书续期判断betweenDay: {}, beforeDays: {}", betweenDay, beforeDays);
        // todo 续期是在过期前操作的，过期了需要新申请证书，此处待修改
        if (betweenDay <= beforeDays) {
            log.info("证书即将过期，触发证书自动续期 openId:{}, expireAt:{}", openId, expireAt);
            // 证书续期
            SccaCertVO sccaCertVO;
            try {
                sccaCertVO = SignSccaUtil.cloudCertRenew(openId, pin);
            } catch (BusinessException e) {
                log.warn("证书有效期校验-证书自动续期失败 openId:{}, errorMsg:{}", openId, e.getMessage());
                throw e;
            }

            X509Certificate x509Certificate;
            try {
                x509Certificate = this.getCertInfoByBase64(sccaCertVO.getCertBase64());
            } catch (BusinessException e) {
                log.warn("证书有效期校验-新续期证书解析失败 openId:{}, newCertBase64:{}", openId, sccaCertVO.getCertBase64());
                throw e;
            }
            // 保存证书信息
            EsignSccaCert sccaCert = esignSccaCertRepository.findByOpenId(openId);
            sccaCert.setNotBefore(x509Certificate.getNotBefore());
            sccaCert.setNotAfter(x509Certificate.getNotAfter());
            sccaCert.setCertSerialnumber(sccaCertVO.getCertSerialnumber());
            sccaCert.setCertP7(sccaCertVO.getCertP7());
            sccaCert.setCertBase64(sccaCertVO.getCertBase64());
            sccaCert.setChangedAt(new Date());
            esignSccaCertRepository.update(sccaCert);
            log.info("证书即将过期，证书自动续期更新完成 openId:{}, expireAt:{}", openId, x509Certificate.getNotAfter());
        }

    }

    /**
     * 根据 SubjectDN 获取指定KEY内容
     *
     * @param subjectDN
     * @return
     */
    private Map<String, String> getCertSubjectDNMap(String subjectDN) {
        String[] subjects = subjectDN.split(",");
        Map<String, String> subjectDNMap = Maps.newHashMap();
        Arrays.stream(subjects).forEach(s -> {
            String[] kvArr = s.split("=");
            subjectDNMap.put(kvArr[0], kvArr[1]);
        });
        return subjectDNMap;
    }
}
